#pragma once

#include "AudioTools/AudioCodecs/AudioEncoded.h"
#include "AudioTools/AudioCodecs/CodecCopy.h"
#include "AudioTools/AudioCodecs/CodecWAV.h"
#include "AudioTools/CoreAudio/AudioStreams.h"

#if VS1053_EXT
#include "VS1053Driver.h"
#else
#include "VS1053.h"
#endif

namespace audio_tools {

enum VS1053Mode { ENCODED_MODE, PCM_MODE, MIDI_MODE };

/**
 * @brief Configuration for VS1053Stream
 * @author Phil Schatzmann
 * @copyright GPLv3
 */
class VS1053Config : public AudioInfo {
 public:
  VS1053Config() {
    sample_rate = 44100;
    channels = 2;
    bits_per_sample = 16;
  }
  /// Operation mode (transmit/receive). Default: TX_MODE (playback).
  RxTxMode mode = TX_MODE;

  /// Chip-select (control) pin for the VS1053 SPI interface.
  /// Set this to the board-specific CS pin (e.g. VS1053_CS).
  uint8_t cs_pin = VS1053_CS;

  /// Data/command select (DCS) pin used by some VS1053 modules.
  uint8_t dcs_pin = VS1053_DCS;

  /// Data request (DREQ) pin — used by the VS1053 to signal it can accept
  /// more data. Configure this to the board's DREQ pin (e.g. VS1053_DREQ).
  uint8_t dreq_pin = VS1053_DREQ;

  /// Optional reset pin for the VS1053 module. Set to -1 if unused.
  /// When valid the pin will be toggled during initialization.
  int16_t reset_pin = VS1053_RESET;  // -1 is undefined

  /// Optional chip-select pin for an attached SD card (if present).
  int16_t cs_sd_pin = VS1053_CS_SD;

  /// When true the stream expects encoded input data (MP3/WAV/etc).
  /// When false (default) the stream treats input as raw PCM samples.
  bool is_encoded_data = false;

  /// When true enable MIDI streaming mode (this also forces encoded mode).
  bool is_midi = false;

  /// If true the driver will call SPI.begin() during initialization. Set
  /// to false if SPI is managed externally by the application.
  bool is_start_spi = true;
#if VS1053_EXT
  VS1053_INPUT input_device = VS1053_MIC;
#endif
};

/**
 * @brief Output Interface which processes PCM data by default using a VS1053
 * audio module. If you want to write encoded data set is_encoded_data = true in
 * the configuration. Many VS1053 modules also have a built in microphone that
 * can be used for recording: set the mode to RX_MODE to use this.
 *
 * Depends on https://github.com/pschatzmann/arduino-vs1053
 *
 * @ingroup io
 * @author Phil Schatzmann
 * @copyright GPLv3
 */
class VS1053Stream : public AudioStream, public VolumeSupport {
  /**
   * @brief Private output class for the EncodedAudioStream
   */
  class VS1053StreamOut : public AudioStream {
   public:
    VS1053StreamOut(VS1053* vs) { p_VS1053 = vs; }
    size_t write(const uint8_t* data, size_t len) override {
      if (p_VS1053 == nullptr) {
        LOGE("NPE");
        return 0;
      }
      TRACED();
      p_VS1053->playChunk((uint8_t*)data, len);
      return len;
    }

   protected:
    VS1053* p_VS1053 = nullptr;
  };

 public:
  VS1053Stream() = default;

  ~VS1053Stream() { end(); }

  /// Provides the default configuration for the indicated mod
  VS1053Config defaultConfig(RxTxMode mode = TX_MODE) {
    TRACED();
    VS1053Config c;
    // recording is rather inefficient so we use a low sample rate as default
    if (mode == RX_MODE) {
      c.sample_rate = 8000;
    }
    c.mode = mode;
    return c;
  }

  /// defines the default configuration that is used with the next begin()
  void setAudioInfo(VS1053Config c) {
    cfg = c;
    AudioStream::setAudioInfo(cfg);
  }

  /// Updates the AudioInfo (sample rate, bits, channels)
  void setAudioInfo(AudioInfo c) {
    cfg.copyFrom(c);
    AudioStream::setAudioInfo(cfg);
  }

  /// Starts with the default config or restarts
  bool begin() { return begin(cfg); }

  /// Starts with the indicated configuration
  bool begin(VS1053Config cfg) {
    TRACEI();
    bool result = true;
    // enfornce encoded data for midi mode
    if (cfg.is_midi) {
      cfg.is_encoded_data = true;
    }
    this->cfg = cfg;
    setAudioInfo(cfg);
    cfg.logInfo();
    LOGI("is_encoded_data: %s", cfg.is_encoded_data ? "true" : "false");
    LOGI("is_midi: %s", cfg.is_midi ? "true" : "false");
    LOGI("cs_pin: %d", cfg.cs_pin);
    LOGI("dcs_pin: %d", cfg.dcs_pin);
    LOGI("dreq_pin: %d", cfg.dreq_pin);
    LOGI("reset_pin: %d", cfg.reset_pin);
    LOGI("cs_sd_pin: %d", cfg.cs_sd_pin);

    if (p_vs1053 == nullptr) {
      p_vs1053 = new VS1053(cfg.cs_pin, cfg.dcs_pin, cfg.dreq_pin);
      p_vs1053_out = new VS1053StreamOut(p_vs1053);

      if (cfg.is_start_spi) {
        LOGI("SPI.begin()")
        SPI.begin();
      } else {
        LOGI("SPI not started");
      }

      if (cfg.reset_pin != -1) {
        LOGI("Setting reset pin to high: %d", cfg.reset_pin);
        pinMode(cfg.reset_pin, OUTPUT);
        digitalWrite(cfg.reset_pin, HIGH);
        delay(800);
      }
    }

    // Output Stream
    if (p_out == nullptr) {
      AudioEncoder* p_enc = cfg.is_encoded_data ? &copy : p_encoder;
      p_out = new EncodedAudioStream(p_vs1053_out, p_enc);
    }

    // hack to treat midi as separate mode
    const int MIDI_MODE = 100;
    int mode = cfg.mode;
    if (cfg.is_midi) {
      mode = MIDI_MODE;
    }

    switch (mode) {
      case TX_MODE:
        result = beginTx();
        break;
#if VS1053_EXT
      case MIDI_MODE:
        result = beginMidi();
        break;

      case RX_MODE:
        result = beginRx();
        break;
#endif
      default:
        LOGD("Mode not supported");
        result = false;
        break;
    }

    // log error on failure
    if (!result) LOGE("begin failed");
    return result;
  }

  /// Stops the processing and releases the memory
  void end() {
    TRACEI();
    if (p_out != nullptr) {
      delete p_out;
      p_out = nullptr;
    }
    // free the wrapper before freeing the underlying HW object to avoid
    // leaving a wrapper with a dangling pointer
    if (p_vs1053_out != nullptr) {
      delete p_vs1053_out;
      p_vs1053_out = nullptr;
    }
    if (p_vs1053 != nullptr) {
      // p_driver->end();
      p_vs1053->stopSong();
      p_vs1053->softReset();
      delete p_vs1053;
      p_vs1053 = nullptr;
    }
  }

  /// value from 0 to 1.0
  bool setVolume(float vol) override {
    // make sure that value is between 0 and 1
    float volume = vol;
    if (volume > 1.0f) volume = 1.0f;
    if (volume < 0.0f) volume = 0.0f;
    LOGD("setVolume: %f", volume);
    if (p_vs1053 != nullptr) {
      // Set the player volume.Level from 0-100, higher is louder
      p_vs1053->setVolume(volume * 100.0f);
    }
    return true;
  }

  /// provides the volume
  float volume() override {
    TRACED();
    if (p_vs1053 == nullptr) return -1.0f;
    return p_vs1053->getVolume() / 100.0f;
  }

  /// Adjusting the left and right volume balance, higher to enhance the right
  /// side, lower to enhance the left side.
  void setBalance(float bal) {
    float balance = bal;
    if (balance < -1.0f) balance = -1.0f;
    if (balance > 1.0f) balance = 1.0f;
    LOGD("setBalance: %f", balance);
    if (p_vs1053 != nullptr) {
      p_vs1053->setBalance(balance * 100.0f);
    }
  }
  /// Get the currenet balance setting (-1.0..1.0)
  float balance() {
    TRACED();
    if (p_vs1053 == nullptr) return -1.0f;
    return static_cast<float>(p_vs1053->getBalance()) / 100.0f;
  }

  /// Write audio data
  virtual size_t write(const uint8_t* data, size_t len) override {
    TRACED();
    if (len == 0) return 0;
    if (p_out == nullptr) {
      LOGE("vs1053 is closed");
      return 0;
    }
    return p_out->write(data, len);
  }

  /// returns the VS1053 object
  VS1053& getVS1053() {
    TRACED();
    return *p_vs1053;
  }

  /// Defines an alternative encoder that will be used (e.g. MP3Encoder). It
  /// must be allocated on the heap!
  bool setEncoder(AudioEncoder* enc) {
    TRACEI();
    if (p_out != nullptr) {
      logError("setEncoder");
      return false;
    }
    // If caller provides nullptr, fall back to the internal wav encoder.
    // Note: ownership remains external (VS1053Stream does not delete the
    // encoder pointer).
    if (enc == nullptr) {
      p_encoder = &wav;
    } else {
      p_encoder = enc;
    }
    return true;
  }

#if VS1053_EXT
  int available() override {
    int result = getVS1053().available();
    LOGI("available: %d", result);
    return result;
  }
  size_t readBytes(uint8_t* data, size_t len) override {
    TRACED();
    return getVS1053().readBytes(data, len);
  }

  /// Provides the treble amplitude value
  float treble() {
    TRACED();
    return static_cast<float>(getVS1053().treble()) / 100.0;
  }

  /// Sets the treble amplitude value (range 0 to 1.0)
  void setTreble(float val) {
    float value = val;
    if (value < 0.0f) value = 0.0f;
    if (value > 1.0f) value = 1.0f;
    LOGD("setTreble: %f", value);
    getVS1053().setTreble(value * 100.0f);
  }

  /// Provides the Bass amplitude value
  float bass() {
    TRACED();
    return static_cast<float>(getVS1053().bass()) / 100.0;
  }

  /// Sets the bass amplitude value (range 0 to 1.0)
  void setBass(float val) {
    float value = val;
    if (value < 0.0f) value = 0.0f;
    if (value > 1.0f) value = 1.0f;
    LOGD("setBass: %f", value);
    getVS1053().setBass(value * 100.0f);
  }

  /// Sets the treble frequency limit in hz (range 0 to 15000)
  void setTrebleFrequencyLimit(uint16_t value) {
    LOGD("setTrebleFrequencyLimit: %u", value);
    getVS1053().setTrebleFrequencyLimit(value);
  }
  /// Sets the bass frequency limit in hz (range 0 to 15000)
  void setBassFrequencyLimit(uint16_t value) {
    LOGD("setBassFrequencyLimit: %u", value);
    getVS1053().setBassFrequencyLimit(value);
  }

  /// Sends a midi message to the VS1053
  void sendMidiMessage(uint8_t cmd, uint8_t data1, uint8_t data2) {
    TRACEI();
#if USE_MIDI
    if (!cfg.is_midi) {
      LOGE("start with is_midi=true");
      return;
    }
    if (p_vs1053 == nullptr) {
      logError(__FUNCTION__);
      return;
    }
    p_vs1053->sendMidiMessage(cmd, data1, data2);
#endif
  }

#endif

 protected:
  VS1053Config cfg;
  VS1053* p_vs1053 = nullptr;
  VS1053StreamOut* p_vs1053_out = nullptr;
  EncodedAudioStream* p_out = nullptr;
  WAVEncoder wav;
  AudioEncoder* p_encoder = &wav;  // by default we send wav data
  CopyEncoder copy;                // used when is_encoded_data == true

  bool beginTx() {
    TRACEI();
    p_out->begin(cfg);
    bool result = p_vs1053->begin();
    p_vs1053->startSong();
    p_vs1053->switchToMp3Mode();  // optional, some boards require this
    if (p_vs1053->getChipVersion() ==
        4) {  // Only perform an update if we really are using a VS1053, not.
              // eg. VS1003
      p_vs1053->loadDefaultVs1053Patches();
    }
    delay(500);
    setVolume(VS1053_DEFAULT_VOLUME);
    return result;
  }

#if VS1053_EXT

  bool beginRx() {
    TRACEI();
    VS1053Recording rec;
    rec.setSampleRate(cfg.sample_rate);
    rec.setChannels(cfg.channels);
    rec.setInput(cfg.input_device);
    return p_vs1053->beginInput(rec);
  }

  bool beginMidi() {
#if USE_MIDI
    TRACEI();
    p_out->begin(cfg);
    bool result = p_vs1053->beginMidi();
    delay(500);
    setVolume(VS1053_DEFAULT_VOLUME);
    return result;
#else
    return false;
#endif
  }

#endif
  void logError(const char* str) { LOGE("Call %s after begin()", str); }
};

}  // namespace audio_tools